/*
 * topological-hex, a program to compute combinatorial hexahedral meshes.
 *
 * Copyright (C) <2018> <Université catholique de Louvain (UCL), Belgique>
 *
 * List of the contributors to the development, description and complete
 * License: see LICENSE file.
 *
 * This program (topological-hex) is free software:
 * you can redistribute it and/or modify it under the terms
 * of the GNU General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program (see COPYING file).  If not,
 * see <http://www.gnu.org/licenses/>.
 */


#include <check.h>
#include <stdlib.h>
#include <stdatomic.h>

#include "dfs.h"
#include "parallel_dfs.h"
#include "input.h"

#define MAX_HEX 50

atomic_uint_fast32_t num_solutions[MAX_HEX+1];

static void on_solution(const solver *solver, uint32_t num_hex, void *data) {
  atomic_fetch_add(num_solutions + num_hex, 1);
}

typedef enum test_flags {
  TEST_DEFAULT,
  TEST_WITH_SYMMETRIES,
  TEST_BIPARTITE_GRAPH,
  TEST_SHELLABLE_ONLY,

  TEST_FLAGS_COUNT,
} test_flags;

static void test_input(const char *file,
                       uint32_t num_hexes, uint32_t num_extra_vertices,
                       uint32_t *expected_sizes,
                       test_flags flags) {
  mesh mesh;
  ck_assert(mesh_load(&mesh, file) == SUCCESS);
  ck_assert(mesh_reserve(&mesh, num_hexes) == SUCCESS);

  uint8_t solver_flags = 0;
  if (flags >= TEST_BIPARTITE_GRAPH) solver_flags |= SOLVER_BIPARTITE_GRAPH;
  if (flags >= TEST_SHELLABLE_ONLY)  solver_flags |= SOLVER_SHELLABLE_ONLY;

  solver solver;
  ck_assert(solver_init(&solver, &mesh,
                        mesh.num_vertices + num_extra_vertices,
                        solver_flags) == SUCCESS);

  if (flags >= TEST_WITH_SYMMETRIES)
    solver_compute_symmetries(&solver);

  for (uint32_t i = 0; i <= MAX_HEX; i++)
    atomic_init(num_solutions + i, 0);

  solver_run(&solver, on_solution, NULL);

  for (uint32_t i = 0; i <= MAX_HEX; i++) {
    ck_assert_msg(atomic_load(num_solutions + i) == expected_sizes[i],
                  "Got %u solution of size %u, expected %u (flags: %u)",
                  atomic_load(num_solutions + i), i, expected_sizes[i],
                  flags);
  }

  solver_release(&solver);
  mesh_release(&mesh);
}

static void test_input_parallel(
  const char *file,
  uint32_t num_hexes, uint32_t num_extra_vertices,
  uint32_t *expected_sizes,
  test_flags flags) {
  mesh mesh;
  ck_assert(mesh_load(&mesh, file) == SUCCESS);
  ck_assert(mesh_reserve(&mesh, num_hexes) == SUCCESS);

  uint8_t solver_flags = 0;
  if (flags >= TEST_BIPARTITE_GRAPH) solver_flags |= SOLVER_BIPARTITE_GRAPH;
  if (flags >= TEST_SHELLABLE_ONLY)  solver_flags |= SOLVER_SHELLABLE_ONLY;

  solver solver;
  ck_assert(solver_init(&solver, &mesh,
                        mesh.num_vertices + num_extra_vertices,
                        solver_flags) == SUCCESS);

  if (flags >= TEST_WITH_SYMMETRIES)
    solver_compute_symmetries(&solver);

  for (uint32_t i = 0; i <= MAX_HEX; i++)
    atomic_init(num_solutions + i, 0);

  ck_assert(solver_run_parallel(&solver, on_solution, NULL) == SUCCESS);

  solver_release(&solver);
  mesh_release(&mesh);

  for (uint32_t i = 0; i <= MAX_HEX; i++) {
    ck_assert_msg(atomic_load(num_solutions + i) == expected_sizes[i],
                  "Got %u solution of size %u, expected %u (flags: %u)",
                  atomic_load(num_solutions + i), i, expected_sizes[i],
                  flags);
  }
}

START_TEST(test_dfs_cube) {
  uint32_t sizes[MAX_HEX+1] = {0};
  sizes[1] = 1;

  for (test_flags flags = 0; flags < TEST_FLAGS_COUNT; flags++) {
    test_input(INPUT_DIR "/single-hex.mesh", MAX_HEX, 0, sizes, flags);
    test_input_parallel(INPUT_DIR "/single-hex.mesh", MAX_HEX, 0, sizes, flags);
  }
} END_TEST

START_TEST(test_dfs_pillowed_cube) {
  uint32_t sizes[MAX_HEX+1] = {0};
  sizes[1] = 1;
  sizes[7] = 1;

  for (test_flags flags = 0; flags < TEST_FLAGS_COUNT; flags++) {
    test_input(INPUT_DIR "/single-hex.mesh", MAX_HEX, 15, sizes, flags);
    test_input_parallel(INPUT_DIR "/single-hex.mesh", MAX_HEX, 15, sizes, flags);
  }
} END_TEST

START_TEST(test_dfs_two_hex) {
  uint32_t sizes[MAX_HEX+1] = {0};
  sizes[2] = 1;
  for (test_flags flags = 0; flags < TEST_FLAGS_COUNT; flags++) {
    test_input(INPUT_DIR "/two-hex.mesh", MAX_HEX, 2, sizes, flags);
    test_input_parallel(INPUT_DIR "/two-hex.mesh", MAX_HEX, 2, sizes, flags);
  }
} END_TEST

START_TEST(test_dfs_prism) {
  uint32_t sizes[MAX_HEX+1] = {0};
  sizes[6] = 1;
  for (test_flags flags = 0; flags < TEST_FLAGS_COUNT; flags++) {
    test_input(INPUT_DIR "/prism.mesh", 8, 1, sizes, flags);
    test_input_parallel(INPUT_DIR "/prism.mesh", 8, 1, sizes, flags);
  }
} END_TEST

START_TEST(test_dfs_tetrahedron) {
  uint32_t sizes[MAX_HEX+1] = {0};
  for (test_flags flags = 0; flags < TEST_FLAGS_COUNT; flags++) {
    sizes[4] = flags >= TEST_WITH_SYMMETRIES ? 1 : 2;

    test_input(INPUT_DIR "/tetrahedron.mesh", MAX_HEX, 1, sizes, flags);
    test_input_parallel(INPUT_DIR "/tetrahedron.mesh", MAX_HEX, 1, sizes, flags);
  }
} END_TEST

START_TEST(test_dfs_pyramid) {
  uint32_t sizes[MAX_HEX+1] = {0};
  for (test_flags flags = 0; flags < TEST_FLAGS_COUNT; flags++) {
    test_input(INPUT_DIR "/pyramid.mesh", MAX_HEX, 1, sizes, flags);
    test_input_parallel(INPUT_DIR "/pyramid.mesh", MAX_HEX, 1, sizes, flags);
  }
} END_TEST

START_TEST(test_dfs_spindle) {
  uint32_t sizes[MAX_HEX+1] = {0};
  for (test_flags flags = 0; flags < TEST_FLAGS_COUNT; flags++) {
    test_input(INPUT_DIR "/spindle.mesh", MAX_HEX, 1, sizes, flags);
    test_input_parallel(INPUT_DIR "/spindle.mesh", MAX_HEX, 1, sizes, flags);
  }
} END_TEST

START_TEST(test_dfs_rhombic_dodecahedron) {
  uint32_t sizes[MAX_HEX+1] = {0};
  sizes[4]  = 1;
  sizes[10] = 1;

  mesh mesh;
  ck_assert(mesh_load(&mesh,
                      INPUT_DIR "/rhombic-dodecahedron.mesh") == SUCCESS);
  ck_assert(mesh_reserve(&mesh, MAX_HEX) == SUCCESS);

  vertex_index *symmetries;
  vertex_index num_boundary_vertices;
  size_t num_symmetries;
  ck_assert(read_symmetries(INPUT_DIR "/rhombic-dodecahedron.symmetries",
                            &symmetries, &num_boundary_vertices,
                            &num_symmetries) == SUCCESS);

  solver solver;
  ck_assert(solver_init(&solver, &mesh, mesh.num_vertices + 10,
                        SOLVER_BIPARTITE_GRAPH) == SUCCESS);

  ck_assert(solver_set_symmetries(&solver, num_symmetries,
                                  num_boundary_vertices, symmetries) ==
            SUCCESS);

  for (uint32_t i = 0; i <= MAX_HEX; i++)
    atomic_init(num_solutions + i, 0);

  ck_assert(solver_run_parallel(&solver, on_solution, NULL) == SUCCESS);

  for (uint32_t i = 0; i <= MAX_HEX; i++) {
    ck_assert_msg(atomic_load(num_solutions + i) == sizes[i],
                  "Got %u solution of size %u, expected %u",
                  atomic_load(num_solutions + i), i, sizes[i]);
  }

  solver_release(&solver);
  free(symmetries);
  mesh_release(&mesh);
} END_TEST

START_TEST(test_dfs_automatic_symmetry) {
  uint32_t sizes[MAX_HEX+1] = {0};
  sizes[4]  = 1;
  sizes[10] = 1;

  mesh mesh;
  ck_assert(mesh_load(&mesh,
                      INPUT_DIR "/rhombic-dodecahedron.mesh") == SUCCESS);
  ck_assert(mesh_reserve(&mesh, MAX_HEX) == SUCCESS);

  solver solver;
  ck_assert(solver_init(&solver, &mesh, mesh.num_vertices + 10,
                        SOLVER_BIPARTITE_GRAPH) == SUCCESS);

  ck_assert(solver_compute_symmetries(&solver) == SUCCESS);

  for (uint32_t i = 0; i <= MAX_HEX; i++)
    atomic_init(num_solutions + i, 0);

  ck_assert(solver_run_parallel(&solver, on_solution, NULL) == SUCCESS);

  for (uint32_t i = 0; i <= MAX_HEX; i++) {
    ck_assert_msg(atomic_load(num_solutions + i) == sizes[i],
                  "Got %u solution of size %u, expected %u",
                  atomic_load(num_solutions + i), i, sizes[i]);
  }

  solver_release(&solver);
  mesh_release(&mesh);
} END_TEST

START_TEST(test_dfs_shellable_no_symmetry) {
  mesh mesh;
  ck_assert(mesh_load(&mesh,
                      INPUT_DIR "/rhombic-dodecahedron.mesh") == SUCCESS);
  ck_assert(mesh_reserve(&mesh, MAX_HEX) == SUCCESS);

  solver solver;
  ck_assert(solver_init(&solver, &mesh, mesh.num_vertices + 2,
                        SOLVER_SHELLABLE_ONLY) == SUCCESS);

  ck_assert(solver_run_parallel(&solver, on_solution, NULL) == SUCCESS);

  solver_release(&solver);
  mesh_release(&mesh);
} END_TEST

START_TEST(test_copy) {
  uint32_t sizes[MAX_HEX+1] = {0};
  sizes[4] = 2;

  mesh mesh;
  ck_assert(mesh_load(&mesh, INPUT_DIR "/tetrahedron.mesh") == SUCCESS);
  ck_assert(mesh_reserve(&mesh, MAX_HEX) == SUCCESS);

  solver solver;
  ck_assert(solver_init(&solver, &mesh, mesh.num_vertices + 1,
                        0) == SUCCESS);

  for (uint32_t i = 0; i <= MAX_HEX; i++)
    atomic_init(num_solutions + i, 0);

  solver.cb = NULL;
  solver.user_data = NULL;

  struct solver copy;
  solver_init_copy(&copy, &solver);

  solver.quad_queue[0] = 2;
  solver.num_occurrences[0] = 0;

  solver_run(&copy, on_solution, 0);

  for (uint32_t i = 0; i <= MAX_HEX; i++) {
    ck_assert_msg(atomic_load(num_solutions + i) == sizes[i],
                  "Got %u solution of size %u, expected %u",
                  atomic_load(num_solutions + i), i, sizes[i]);
  }

  solver_release_copy(&copy);
  solver_release(&solver);
  mesh_release(&mesh);
} END_TEST

START_TEST(test_copy_symmetries) {
  mesh mesh;
  ck_assert(mesh_load(&mesh, INPUT_DIR "/spindle.mesh") == SUCCESS);
  ck_assert(mesh_reserve(&mesh, MAX_HEX) == SUCCESS);

  solver solver;
  ck_assert(solver_init(&solver, &mesh, mesh.num_vertices + 1, 0) == SUCCESS);

  for (uint32_t i = 0; i <= MAX_HEX; i++)
    atomic_init(num_solutions + i, 0);

  solver.cb = NULL;
  solver.user_data = NULL;

  vertex_index permutations[] = {
    0, 1, 8, 7, 6, 5, 4, 3, 2, 9,
    1, 0, 3, 4, 5, 6, 7, 8, 9, 2,
    0, 1, 4, 3, 2, 9, 8, 7, 6, 5,
  };

  ck_assert(solver_set_symmetries(&solver, 3, 10, permutations) == SUCCESS);

  ck_assert_int_eq(solver.num_symmetries, 3);
  ck_assert_int_eq(solver.num_boundary_vertices, 10);
  ck_assert_mem_eq(solver.boundary_group, permutations, sizeof(permutations));

  struct solver copy;
  solver_init_copy(&copy, &solver);

  ck_assert_int_eq(copy.num_symmetries, solver.num_symmetries);
  ck_assert_int_eq(copy.num_boundary_vertices, solver.num_boundary_vertices);
  ck_assert_mem_eq(copy.boundary_group, permutations, sizeof(permutations));

  solver_release_copy(&copy);
  solver_release(&solver);
  mesh_release(&mesh);
} END_TEST

Suite *solver_suite(void) {
  Suite *s = suite_create("Solver");

  TCase *tcase = tcase_create("DFS");
  tcase_set_timeout(tcase, 15.0);
  tcase_add_test(tcase, test_dfs_cube);
  tcase_add_test(tcase, test_dfs_pillowed_cube);
  tcase_add_test(tcase, test_dfs_two_hex);
  tcase_add_test(tcase, test_dfs_prism);
  tcase_add_test(tcase, test_dfs_tetrahedron);
  tcase_add_test(tcase, test_dfs_pyramid);
  tcase_add_test(tcase, test_dfs_spindle);
  tcase_add_test(tcase, test_dfs_rhombic_dodecahedron);
  tcase_add_test(tcase, test_dfs_automatic_symmetry);
  tcase_add_test(tcase, test_dfs_shellable_no_symmetry);
  suite_add_tcase(s, tcase);

  tcase = tcase_create("Copy");
  tcase_add_test(tcase, test_copy);
  tcase_add_test(tcase, test_copy_symmetries);
  suite_add_tcase(s, tcase);

  return s;
}
